package drds.connection_pool;


import drds.connection_pool.proxy.ConnectionProxy;
import drds.connection_pool.util.ConcurrentBag;
import drds.connection_pool.util.ConcurrentBag.IBagStateListener;
import drds.connection_pool.util.SuspendResumeLock;
import drds.connection_pool.util.UtilityElf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLTransientConnectionException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.*;

import static drds.connection_pool.util.IConcurrentBagEntry.STATE_IN_USE;
import static drds.connection_pool.util.IConcurrentBagEntry.STATE_NOT_IN_USE;
import static drds.connection_pool.util.clock.Clock.*;
import static java.util.Collections.unmodifiableCollection;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * This is the primary connectionProxy proxy class that provides the basic
 * pooling behavior for HikariCP.
 *
 * @author Brett Wooldridge
 */
public final class Pool extends AbstractPool implements IBagStateListener
{
   public static final int POOL_NORMAL = 0;
   public static final int POOL_SUSPENDED = 1;
   public static final int POOL_SHUTDOWN = 2;
   private static final String EVICTED_CONNECTION_MESSAGE = "(connectionProxy was evicted)";
   private static final String DEAD_CONNECTION_MESSAGE = "(connectionProxy is dead)";
   private final Logger logger = LoggerFactory.getLogger(Pool.class);
   private final long aliveBypassWindowMs = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500));
   private final long housekeepingPeriodMs = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30));
   private final PoolEntryCreator poolEntryCreator = new PoolEntryCreator(null /*logging prefix*/);
   private final PoolEntryCreator postFillPoolEntryCreator = new PoolEntryCreator("After adding ");
   private final Collection<Runnable> addConnectionQueue;
   private final ThreadPoolExecutor addConnectionExecutor;
   private final ThreadPoolExecutor closeConnectionExecutor;
   private final ConcurrentBag<PoolEntry> connectionBag;
   private final ProxyLeakTaskFactory leakTaskFactory;
   private final SuspendResumeLock suspendResumeLock;
   private final ScheduledExecutorService houseKeepingExecutorService;
   public volatile int poolState;
   private ScheduledFuture<?> houseKeeperTask;

   /**
    * Construct a Pool with the specified configuration.
    *
    * @param config a Configuration instance
    */
   public Pool(final Configuration config)
   {
      super(config);

      this.connectionBag = new ConcurrentBag<>(this);
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();

      checkFailFast();


      handleMBeans(this, true);

      ThreadFactory threadFactory = config.getThreadFactory();

      LinkedBlockingQueue<Runnable> addQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
      this.addConnectionQueue = unmodifiableCollection(addQueue);
      this.addConnectionExecutor = UtilityElf.createThreadPoolExecutor(addQueue, poolName + " connectionProxy adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
      this.closeConnectionExecutor = UtilityElf.createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connectionProxy closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

      this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);

      this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

      if (Boolean.getBoolean("com.zaxxer.hikari.blockUntilFilled") && config.getInitializationFailTimeout() > 1)
      {
         final long startTime = currentTime();
         while (elapsedMillis(startTime) < config.getInitializationFailTimeout() && getTotalConnections() < config.getMinimumIdle())
         {
            UtilityElf.quietlySleep(MILLISECONDS.toMillis(100));
         }
      }
   }

   /**
    * Get a connectionProxy from the proxy, or timeout after connectionTimeout milliseconds.
    *
    * @return a java.sql.Connection instance
    * @throws SQLException thrown if a timeout occurs trying to obtain a connectionProxy
    */
   public Connection getConnection() throws SQLException
   {
      return getConnection(connectionTimeout);
   }

   /**
    * Get a connectionProxy from the proxy, or timeout after the specified number of milliseconds.
    *
    * @param hardTimeout the maximum time to wait for a connectionProxy from the proxy
    * @return a java.sql.Connection instance
    * @throws SQLException thrown if a timeout occurs trying to obtain a connectionProxy
    */
   public Connection getConnection(final long hardTimeout) throws SQLException
   {
      suspendResumeLock.acquire();
      final long startTime = currentTime();

      try
      {
         long timeout = hardTimeout;
         do
         {
            PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null)
            {
               break; // We timed out... break and throw exception
            }

            final long now = currentTime();
            if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection)))
            {
               closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
               timeout = hardTimeout - elapsedMillis(startTime);
            } else
            {

               return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
            }
         } while (timeout > 0L);


         throw createTimeoutException(startTime);
      } catch (InterruptedException e)
      {
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Interrupted during connectionProxy acquisition", e);
      } finally
      {
         suspendResumeLock.release();
      }
   }

   /**
    * Shutdown the proxy, closing all idle connections and aborting or closing
    * active connections.
    *
    * @throws InterruptedException thrown if the thread is interrupted during shutdown
    */
   public synchronized void shutdown() throws InterruptedException
   {
      try
      {
         poolState = POOL_SHUTDOWN;

         if (addConnectionExecutor == null)
         { // proxy never started
            return;
         }

         logPoolState("Before shutdown ");

         if (houseKeeperTask != null)
         {
            houseKeeperTask.cancel(false);
            houseKeeperTask = null;
         }

         softEvictConnections();

         addConnectionExecutor.shutdown();
         addConnectionExecutor.awaitTermination(getLoginTimeout(), SECONDS);

         destroyHouseKeepingExecutorService();

         connectionBag.close();

         final ExecutorService assassinExecutor = UtilityElf.createThreadPoolExecutor(configuration.getMaximumPoolSize(), poolName + " connectionProxy assassinator",
            configuration.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
         try
         {
            final long start = currentTime();
            do
            {
               abortActiveConnections(assassinExecutor);
               softEvictConnections();
            } while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(10));
         } finally
         {
            assassinExecutor.shutdown();
            assassinExecutor.awaitTermination(10L, SECONDS);
         }

         shutdownNetworkTimeoutExecutor();
         closeConnectionExecutor.shutdown();
         closeConnectionExecutor.awaitTermination(10L, SECONDS);
      } finally
      {
         logPoolState("After shutdown ");
         handleMBeans(this, false);

      }
   }

   /**
    * Evict a Connection from the proxy.
    *
    * @param connection the Connection to evict (actually a {@link ConnectionProxy})
    */
   public void evictConnection(Connection connection)
   {
      ConnectionProxy connectionProxy = (ConnectionProxy) connection;
      connectionProxy.cancelLeakTask();

      try
      {
         softEvictConnection(connectionProxy.getPoolEntry(), "(connectionProxy evicted by user)", !connection.isClosed() /* owner */);
      } catch (SQLException e)
      {
         // unreachable in HikariCP, but we're still forced to catch it
      }
   }


   // ***********************************************************************
   //                        IBagStateListener callback
   // ***********************************************************************

   /**
    * {@inheritDoc}
    */
   @Override
   public void addBagItem(final int waiting)
   {
      final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
      if (shouldAdd)
      {
         addConnectionExecutor.submit(poolEntryCreator);
      }
   }

   // ***********************************************************************
   //                        HikariPoolMBean methods
   // ***********************************************************************

   /**
    * {@inheritDoc}
    */

   public int getActiveConnections()
   {
      return connectionBag.getCount(STATE_IN_USE);
   }

   /**
    * {@inheritDoc}
    */
   public int getIdleConnections()
   {
      return connectionBag.getCount(STATE_NOT_IN_USE);
   }

   /**
    * {@inheritDoc}
    */
   public int getTotalConnections()
   {
      return connectionBag.size();
   }

   /**
    * {@inheritDoc}
    */

   public int getThreadsAwaitingConnection()
   {
      return connectionBag.getWaitingThreadCount();
   }

   /**
    * {@inheritDoc}
    */

   public void softEvictConnections()
   {
      connectionBag.values().forEach(poolEntry -> softEvictConnection(poolEntry, "(connectionProxy evicted)", false /* not owner */));
   }

   /**
    * {@inheritDoc}
    */

   public synchronized void suspendPool()
   {
      if (suspendResumeLock == SuspendResumeLock.FAUX_LOCK)
      {
         throw new IllegalStateException(poolName + " - is not suspendable");
      } else if (poolState != POOL_SUSPENDED)
      {
         suspendResumeLock.suspend();
         poolState = POOL_SUSPENDED;
      }
   }

   /**
    * {@inheritDoc}
    */

   public synchronized void resumePool()
   {
      if (poolState == POOL_SUSPENDED)
      {
         poolState = POOL_NORMAL;
         fillPool();
         suspendResumeLock.resume();
      }
   }

   // ***********************************************************************
   //                           Package methods
   // ***********************************************************************

   /**
    * Log the current proxy state at debug level.
    *
    * @param prefix an optional prefix to prepend the log message
    */
   public void logPoolState(String... prefix)
   {
      if (logger.isDebugEnabled())
      {
         logger.debug("{} - {}stats (total={}, active={}, idle={}, waiting={})",
            poolName, (prefix.length > 0 ? prefix[0] : ""),
            getTotalConnections(), getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection());
      }
   }

   /**
    * Recycle PoolEntry (add back to the proxy)
    *
    * @param poolEntry the PoolEntry to recycle
    */
   @Override
   void recycle(final PoolEntry poolEntry)
   {


      connectionBag.requite(poolEntry);
   }

   /**
    * Permanently close the real (underlying) connectionProxy (eat any exception).
    *
    * @param poolEntry     poolEntry having the connectionProxy to close
    * @param closureReason reason to close
    */
   void closeConnection(final PoolEntry poolEntry, final String closureReason)
   {
      if (connectionBag.remove(poolEntry))
      {
         final Connection connection = poolEntry.close();
         closeConnectionExecutor.execute(() ->
         {
            quietlyCloseConnection(connection, closureReason);
            if (poolState == POOL_NORMAL)
            {
               fillPool();
            }
         });
      }
   }

   @SuppressWarnings("unused")
   int[] getPoolStateCounts()
   {
      return connectionBag.getStateCounts();
   }


   // ***********************************************************************
   //                           Private methods
   // ***********************************************************************

   /**
    * Creating new poolEntry.  If maxLifetime is configured, create a future End-of-life task with 2.5% variance from
    * the maxLifetime time to ensure there is no massive die-off of Connections in the proxy.
    */
   private PoolEntry createPoolEntry()
   {
      try
      {
         final PoolEntry poolEntry = newPoolEntry();

         final long maxLifetime = configuration.getMaxLifetime();
         if (maxLifetime > 0)
         {
            // variance up to 2.5% of the maxlifetime
            final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong(maxLifetime / 40) : 0;
            final long lifetime = maxLifetime - variance;
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
               () ->
               {
                  if (softEvictConnection(poolEntry, "(connectionProxy has passed maxLifetime)", false /* not owner */))
                  {
                     addBagItem(connectionBag.getWaitingThreadCount());
                  }
               },
               lifetime, MILLISECONDS));
         }

         return poolEntry;
      } catch (ConnectionSetupException e)
      {
         if (poolState == POOL_NORMAL)
         { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.error("{} - Error thrown while acquiring connectionProxy from data source", poolName, e.getCause());
            lastConnectionException.set(e);
         }
         return null;
      } catch (SQLException e)
      {
         if (poolState == POOL_NORMAL)
         { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.debug("{} - Cannot acquire connectionProxy from data source", poolName, e);
            lastConnectionException.set(new ConnectionSetupException(e));
         }
         return null;
      } catch (Exception e)
      {
         if (poolState == POOL_NORMAL)
         { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            logger.error("{} - Error thrown while acquiring connectionProxy from data source", poolName, e);
            lastConnectionException.set(new ConnectionSetupException(e));
         }
         return null;
      }
   }

   /**
    * Fill proxy up from current idle connections (as they are perceived at the point of execution) to minimumIdle connections.
    */
   private synchronized void fillPool()
   {
      final int connectionsToAdd = Math.min(configuration.getMaximumPoolSize() - getTotalConnections(), configuration.getMinimumIdle() - getIdleConnections())
         - addConnectionQueue.size();
      for (int i = 0; i < connectionsToAdd; i++)
      {
         addConnectionExecutor.submit((i < connectionsToAdd - 1) ? poolEntryCreator : postFillPoolEntryCreator);
      }
   }

   /**
    * Attempt to abort or close active connections.
    *
    * @param assassinExecutor the ExecutorService to pass to Connection.abort()
    */
   private void abortActiveConnections(final ExecutorService assassinExecutor)
   {
      for (PoolEntry poolEntry : connectionBag.values(STATE_IN_USE))
      {
         Connection connection = poolEntry.close();
         try
         {
            connection.abort(assassinExecutor);
         } catch (Throwable e)
         {
            quietlyCloseConnection(connection, "(connectionProxy aborted during shutdown)");
         } finally
         {
            connectionBag.remove(poolEntry);
         }
      }
   }

   /**
    * If initializationFailFast is configured, check that we have DB connectivity.
    *
    * @throws PoolInitializationException if fails to create or validate connectionProxy
    * @see Configuration#setInitializationFailTimeout(long)
    */
   private void checkFailFast()
   {
      final long initializationTimeout = configuration.getInitializationFailTimeout();
      if (initializationTimeout < 0)
      {
         return;
      }

      final long startTime = currentTime();
      do
      {
         final PoolEntry poolEntry = createPoolEntry();
         if (poolEntry != null)
         {
            if (configuration.getMinimumIdle() > 0)
            {
               connectionBag.add(poolEntry);
               logger.debug("{} - Added connectionProxy {}", poolName, poolEntry.connection);
            } else
            {
               quietlyCloseConnection(poolEntry.close(), "(initialization check complete and minimumIdle is zero)");
            }

            return;
         }

         if (getLastConnectionException() instanceof ConnectionSetupException)
         {
            throwPoolInitializationException(getLastConnectionException().getCause());
         }

         UtilityElf.quietlySleep(SECONDS.toMillis(1));
      } while (elapsedMillis(startTime) < initializationTimeout);

      if (initializationTimeout > 0)
      {
         throwPoolInitializationException(getLastConnectionException());
      }
   }

   /**
    * Log the Throwable that caused proxy initialization to fail, and then throw a PoolInitializationException with
    * that cause attached.
    *
    * @param t the Throwable that caused the proxy to fail to initialize (possibly null)
    */
   private void throwPoolInitializationException(Throwable t)
   {
      logger.error("{} - Exception during proxy initialization.", poolName, t);
      destroyHouseKeepingExecutorService();
      throw new PoolInitializationException(t);
   }

   /**
    * "Soft" evict a Connection (/PoolEntry) from the proxy.  If this method is being called by the user directly
    * through {@link ConnectionPool#evictConnection(Connection)} then {@code owner} is {@code true}.
    * <p>
    * If the caller is the owner, or if the Connection is idle (i.e. can be "reserved" in the {@link ConcurrentBag}),
    * then we can close the connectionProxy immediately.  Otherwise, we leave it "marked" for eviction so that it is evicted
    * the next time someone tries to acquire it from the proxy.
    *
    * @param poolEntry the PoolEntry (/Connection) to "soft" evict from the proxy
    * @param reason    the reason that the connectionProxy is being evicted
    * @param owner     true if the caller is the owner of the connectionProxy, false otherwise
    * @return true if the connectionProxy was evicted (closed), false if it was merely marked for eviction
    */
   private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner)
   {
      poolEntry.markEvicted();
      if (owner || connectionBag.reserve(poolEntry))
      {
         closeConnection(poolEntry, reason);
         return true;
      }

      return false;
   }

   /**
    * Create/initialize the Housekeeping service {@link ScheduledExecutorService}.  If the user specified an Executor
    * to be used in the {@link Configuration}, then we use that.  If no Executor was specified (typical), then create
    * an Executor and configure it.
    *
    * @return either the user specified {@link ScheduledExecutorService}, or the one we created
    */
   private ScheduledExecutorService initializeHouseKeepingExecutorService()
   {
      if (configuration.getScheduledExecutorService() == null)
      {
         final ThreadFactory threadFactory = Optional.ofNullable(configuration.getThreadFactory()).orElseGet(() -> new UtilityElf.DefaultThreadFactory(poolName + " housekeeper", true));
         final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy());
         executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
         executor.setRemoveOnCancelPolicy(true);
         return executor;
      } else
      {
         return configuration.getScheduledExecutorService();
      }
   }

   /**
    * Destroy (/shutdown) the Housekeeping service Executor, if it was the one that we created.
    */
   private void destroyHouseKeepingExecutorService()
   {
      if (configuration.getScheduledExecutorService() == null)
      {
         houseKeepingExecutorService.shutdownNow();
      }
   }

   /**
    * Create a PoolState instance that will be used by metrics tracking, with a pollable resolution of 1 second.
    *
    * @return a PoolState instance
    */
   private PoolState getPoolState()
   {
      return new PoolState(SECONDS.toMillis(1))
      {
         @Override
         protected void update()
         {
            this.pendingThreads = Pool.this.getThreadsAwaitingConnection();
            this.idleConnections = Pool.this.getIdleConnections();
            this.totalConnections = Pool.this.getTotalConnections();
            this.activeConnections = Pool.this.getActiveConnections();
            this.maxConnections = configuration.getMaximumPoolSize();
            this.minConnections = configuration.getMinimumIdle();
         }
      };
   }

   /**
    * Create a timeout exception (specifically, {@link SQLTransientConnectionException}) to be thrown, because a
    * timeout occurred when trying to acquire a Connection from the proxy.  If there was an underlying cause for the
    * timeout, e.g. a SQLException thrown by the driver while trying to create a new Connection, then use the
    * SQL State from that exception as our own and additionally set that exception as the "next" SQLException inside
    * of our exception.
    * <p>
    * As a side-effect, log the timeout failure at DEBUG, and record the timeout failure in the metrics tracker.
    *
    * @param startTime the start time (timestamp) of the acquisition attempt
    * @return a SQLException to be thrown from {@link #getConnection()}
    */
   private SQLException createTimeoutException(long startTime)
   {
      logPoolState("Timeout failure ");


      String sqlState = null;
      final Throwable originalException = getLastConnectionException();
      if (originalException instanceof SQLException)
      {
         sqlState = ((SQLException) originalException).getSQLState();
      }
      final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, originalException);
      if (originalException instanceof SQLException)
      {
         connectionException.setNextException((SQLException) originalException);
      }

      return connectionException;
   }


   // ***********************************************************************
   //                      Non-anonymous Inner-classes
   // ***********************************************************************

   public static class PoolInitializationException extends RuntimeException
   {
      private static final long serialVersionUID = 929872118275916520L;

      /**
       * Construct an exception, possibly wrapping the provided Throwable as the cause.
       *
       * @param t the Throwable to wrap
       */
      public PoolInitializationException(Throwable t)
      {
         super("Failed to initialize proxy: " + t.getMessage(), t);
      }
   }

   /**
    * Creating and adding poolEntries (connections) to the proxy.
    */
   private final class PoolEntryCreator implements Callable<Boolean>
   {
      private final String loggingPrefix;

      PoolEntryCreator(String loggingPrefix)
      {
         this.loggingPrefix = loggingPrefix;
      }

      @Override
      public Boolean call()
      {
         long sleepBackoff = 250L;
         while (poolState == POOL_NORMAL && shouldCreateAnotherConnection())
         {
            final PoolEntry poolEntry = createPoolEntry();
            if (poolEntry != null)
            {
               connectionBag.add(poolEntry);
               logger.debug("{} - Added connectionProxy {}", poolName, poolEntry.connection);
               if (loggingPrefix != null)
               {
                  logPoolState(loggingPrefix);
               }
               return Boolean.TRUE;
            }

            // failed to get connectionProxy from db, sleep and retry
            UtilityElf.quietlySleep(sleepBackoff);
            sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5)));
         }
         // Pool is suspended or shutdown or at max size
         return Boolean.FALSE;
      }

      /**
       * We only create connections if we need another idle connectionProxy or have threads still waiting
       * for a new connectionProxy.  Otherwise we bail out of the request to create.
       *
       * @return true if we should create a connectionProxy, false if the need has disappeared
       */
      private boolean shouldCreateAnotherConnection()
      {
         return getTotalConnections() < configuration.getMaximumPoolSize() &&
            (connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < configuration.getMinimumIdle());
      }
   }

   /**
    * The house keeping task to retire and maintain minimum idle connections.
    */
   private final class HouseKeeper implements Runnable
   {
      private volatile long previous = plusMillis(currentTime(), -housekeepingPeriodMs);

      @Override
      public void run()
      {
         try
         {
            // refresh values in case they changed via MBean
            connectionTimeout = configuration.getConnectionTimeout();
            validationTimeout = configuration.getValidationTimeout();
            leakTaskFactory.updateLeakDetectionThreshold(configuration.getLeakDetectionThreshold());
            catalog = (configuration.getCatalog() != null && !configuration.getCatalog().equals(catalog)) ? configuration.getCatalog() : catalog;

            final long idleTimeout = configuration.getIdleTimeout();
            final long now = currentTime();

            // Detect retrograde time, allowing +128ms as per NTP spec.
            if (plusMillis(now, 128) < plusMillis(previous, housekeepingPeriodMs))
            {
               logger.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from proxy.",
                  poolName, elapsedDisplayString(previous, now));
               previous = now;
               softEvictConnections();
               return;
            } else if (now > plusMillis(previous, (3 * housekeepingPeriodMs) / 2))
            {
               // No point evicting for forward clock motion, this merely accelerates connectionProxy retirement anyway
               logger.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
            }

            previous = now;

            String afterPrefix = "Pool ";
            if (idleTimeout > 0L && configuration.getMinimumIdle() < configuration.getMaximumPoolSize())
            {
               logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";

               final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
               int toRemove = notInUse.size() - configuration.getMinimumIdle();
               for (PoolEntry entry : notInUse)
               {
                  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry))
                  {
                     closeConnection(entry, "(connectionProxy has passed idleTimeout)");
                     toRemove--;
                  }
               }
            }

            logPoolState(afterPrefix);

            fillPool(); // Try to maintain minimum connections
         } catch (Exception e)
         {
            logger.error("Unexpected exception in housekeeping task", e);
         }
      }
   }
}
